/*
 * @(#)ScriptUtil.java $version 2010. 8. 11
 *
 * Copyright 2007-2010 NHN Corp. All rights Reserved. 
 * NHN PROPRIETARY/CONFIDENTIAL. Use is subject to license terms.
 */
package com.nhncorp.usf.core.util;

import java.io.*;
import java.net.URL;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;
import java.util.zip.ZipEntry;

import javax.script.Bindings;
import javax.script.ScriptContext;
import javax.script.ScriptEngine;
import javax.script.ScriptException;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

import com.nhncorp.lucy.common.config.model.ApplicationInfo;
import com.nhncorp.lucy.common.util.ResourceLoader;
import com.nhncorp.usf.core.Constants;
import com.nhncorp.usf.core.config.runtime.ScriptEngineGlobals;
import com.nhncorp.usf.core.config.runtime.ScriptInfo;

/**
 * @author Web Platform Development Team
 */
public class ScriptUtil {
	static ResourceLoader resourceLoader = new ResourceLoader();
	static Log log = LogFactory.getLog(ScriptUtil.class);

	/**
	 * Load js lib.
	 * 
	 * @param modulePath the module path
	 * 
	 * @throws IOException Signals that an I/O exception has occurred.
	 * @throws ScriptException the script exception
	 */
	public static void loadJSLib(String modulePath) throws IOException, ScriptException {
		URL jarUrl = resourceLoader.getResource(ScriptEngineGlobals.DEFAULT_SCRIPT_LIBRARY_FILENAME);

		if (jarUrl != null) {
			String dir = makeDir(jarUrl);
			String jarPath = jarUrl.getFile();
			JarFile orginJarFile = null;
			try {
				orginJarFile = new JarFile(jarPath);
				Enumeration<JarEntry> jarEntries = orginJarFile.entries();

				while (jarEntries.hasMoreElements()) {
					JarEntry jarEntry = jarEntries.nextElement();
					String fileName = jarEntry.getName();

					if (fileName.endsWith(".groovy") || fileName.endsWith(".js") || fileName.endsWith(".rb")) {
						String scriptFileName = jarEntry.getName();
						String scriptFile = dir + "/" + scriptFileName;

						//if (scriptFile != null && orginJarFile != null) {
						decompressJarFile(scriptFile, jarEntry, orginJarFile);
						addScriptEngine(scriptFile, scriptFileName, modulePath);
						//}
					}
				}
			} catch (IOException e) {
				log.debug("fail to create jar file : " + jarPath);
				throw e;
			} finally {
				if (orginJarFile != null) {
					orginJarFile.close();
				}
			}
		}
	}

	/**
	 * Make dir.
	 * 
	 * @param jarUrl the jar url
	 * 
	 * @return the string
	 */
	private static String makeDir(URL jarUrl) {
		String jarFile = jarUrl.getPath().substring(1);
		String dir = jarFile.substring(0, jarFile.lastIndexOf("scriptlib.jar")) + "scripts";
		File dirFile = new File(dir);
		dirFile.mkdirs();
		return dir;
	}

	/**
	 * Decompress jar file.
	 * 
	 * @param scriptFile the script file
	 * @param jarEntry the jar entry
	 * @param orginJarFile the orgin jar file
	 * 
	 * @throws IOException Signals that an I/O exception has occurred.
	 * @throws ScriptException the script exception
	 */
	private static void decompressJarFile(String scriptFile, JarEntry jarEntry, JarFile orginJarFile) throws IOException,
		ScriptException {
		FileOutputStream outstr = null;
		InputStream instr = null;
		try {
			outstr = makeFileOf(scriptFile);
			instr = orginJarFile.getInputStream(jarEntry);
			strcopy(instr, outstr);
		} catch (IOException e) {
			log.debug("fail to unjar script library scriptlib.jar : " + scriptFile);
			throw e;
		} finally {
			try {
				if (instr != null) {
					instr.close();
				}
			} catch (IOException e) {
				log.debug("fail to close InputStream : " + scriptFile);
			}
			try {
				if (outstr != null) {
					outstr.close();
				}
			} catch (IOException e) {
				log.debug("fail to close OutputStream : " + scriptFile);
			}
		}
	}

	/**
	 * Adds the script engine.
	 * 
	 * @param scriptFile the script file
	 * @param scriptFileName the script file name
	 * @param modulePath the module path
	 * 
	 * @throws ScriptException the script exception
	 * @throws FileNotFoundException the file not found exception
	 * @throws UnsupportedEncodingException the unsupported encoding exception
	 */
	@SuppressWarnings("unchecked")
	private static void addScriptEngine(String scriptFile, String scriptFileName, String modulePath) throws ScriptException,
		FileNotFoundException,
		UnsupportedEncodingException {
		File scriptfile = new File(scriptFile);
		String engineType = ScriptEngineGlobals.getScriptEngineType(scriptFileName);
		Map<String, ScriptEngine> engineMap = ScriptEngineGlobals.getScriptEngineForWebModule(modulePath);
		ScriptEngine scriptEngine = null;

		if (engineMap == null) {
			engineMap = new HashMap<String, ScriptEngine>();
		} else {
			ScriptEngine engine = engineMap.get(engineType);

			if (engine != null) {
				scriptEngine = engine;
			}
		}
		if (scriptEngine == null) {
			scriptEngine = ScriptEngineGlobals.SCRIPTENGINE_MANAGER.getEngineByName(engineType);

			if (scriptEngine == null) {
				throw new ScriptException("fail to create script engine : " + engineType);
			}

			Bindings bindings = scriptEngine.createBindings();
			scriptEngine.setBindings(bindings, ScriptContext.ENGINE_SCOPE);
		}

		addScriptEngine(scriptEngine, scriptfile, engineMap, modulePath, engineType);
	}

	/**
	 * Make file of.
	 * 
	 * @param fileName the file name
	 * 
	 * @return the file output stream
	 * 
	 * @throws FileNotFoundException the file not found exception
	 */
	public static FileOutputStream makeFileOf(String fileName) throws FileNotFoundException {
		File file = new File(fileName);

		if (file.getParent() != null) {
			new File(file.getParent()).mkdirs();
		}

		try {
			return new FileOutputStream(file);
		} catch (FileNotFoundException e) {
			log.debug("script library files don't exist : " + fileName);
			throw e;
		}
	}

	/**
	 * Checks if is directory.
	 * 
	 * @param entry the entry
	 * 
	 * @return true, if is directory
	 */
	public static boolean isDirectory(ZipEntry entry) {
		return entry.getName().endsWith("/") || entry.getName().endsWith(File.separator);
	}

	/**
	 * Strcopy.
	 * 
	 * @param instr the instr
	 * @param outstr the outstr
	 * 
	 * @throws IOException Signals that an I/O exception has occurred.
	 */
	public static void strcopy(InputStream instr, OutputStream outstr) throws IOException {
		byte[] buf = new byte[4 * 1024];
		int len;

		while ((len = instr.read(buf)) > 0) {
			outstr.write(buf, 0, len);
		}
	}

	/**
	 * Load property.
	 * 
	 * @param scriptDir the script dir
	 * @param scriptPropertyFile the script property file
	 * 
	 * @throws IOException Signals that an I/O exception has occurred.
	 * @throws ScriptException the script exception
	 */
	public static void loadProperty(String scriptDir, String scriptPropertyFile) throws IOException, ScriptException {
		InputStream in = resourceLoader.getResourceAsStream(scriptPropertyFile);

		if (in != null) {
			BufferedReader br = new BufferedReader(new InputStreamReader(in));
			String scriptFileName;
			try {
				while ((scriptFileName = br.readLine()) != null) {
					File script = new File(scriptDir, scriptFileName);

					if (script != null && script.exists()) {
						createScriptEngine(scriptDir, script);
					}
				}
			} catch (FileNotFoundException e) {
				throw e;
			} catch (IOException e) {
				log.debug("fail to load property file : " + scriptPropertyFile);
				throw e;
			}
			if (br != null) {
				br.close();
			}
		}
	}

	/**
	 * Load js file.
	 * 
	 * @param scriptDir the script dir
	 * 
	 * @throws FileNotFoundException the file not found exception
	 * @throws ScriptException the script exception
	 * @throws UnsupportedEncodingException the unsupported encoding exception
	 */
	public static void loadJSFile(String scriptDir) throws FileNotFoundException,
		ScriptException,
		UnsupportedEncodingException {
		File jsFileDir = new File(scriptDir);

		if (jsFileDir.isDirectory()) {
			File[] scripts = jsFileDir.listFiles();

			if (scripts != null) {
				for (int i = 0; i < scripts.length; i++) {
					if (scripts[i].exists()) {
						createScriptEngine(scriptDir, scripts[i]);
					}
				}
			}
		}
	}

	/**
	 * Creates the script engine.
	 * 
	 * @param modulePath the module path
	 * @param file the file
	 * 
	 * @throws FileNotFoundException the file not found exception
	 * @throws ScriptException the script exception
	 * @throws UnsupportedEncodingException the unsupported encoding exception
	 */
	@SuppressWarnings("unchecked")
	public static void createScriptEngine(String modulePath, File file) throws FileNotFoundException,
		ScriptException,
		UnsupportedEncodingException {
		String fileName = file.getName();

		if (fileName.endsWith(".groovy") || fileName.endsWith(".js") || fileName.endsWith(".rb")) {
			String engineType = ScriptEngineGlobals.getScriptEngineType(file.getName());
			ScriptEngine scriptEngine = ScriptEngineGlobals.getScriptEngine(engineType);

			if (scriptEngine == null) {
				scriptEngine = ScriptEngineGlobals.SCRIPTENGINE_MANAGER.getEngineByName(engineType);

				if (scriptEngine == null) {
					throw new ScriptException("fail to create script engine : " + engineType);
				}
			}

			Map<String, ScriptEngine> engineMap = ScriptEngineGlobals.getScriptEngineForWebModule(modulePath);

			if (engineMap == null) {
				engineMap = new HashMap<String, ScriptEngine>();
			} else {
				ScriptEngine engine = engineMap.get(engineType);

				if (engine != null) {
					scriptEngine = engine;
				}
			}

			addScriptEngine(scriptEngine, file, engineMap, modulePath, engineType);
		}
	}

	/**
	 * Adds the script engine.
	 * 
	 * @param engine the script engine
	 * @param file the file
	 * @param engineMap the engine map
	 * @param modulePath the module path
	 * @param engineType the engine type
	 * 
	 * @throws UnsupportedEncodingException the unsupported encoding exception
	 * @throws FileNotFoundException the file not found exception
	 * @throws ScriptException the script exception
	 */
	private static void addScriptEngine(ScriptEngine engine, File file, Map<String, ScriptEngine> engineMap,
		String modulePath, String engineType) throws UnsupportedEncodingException,
		FileNotFoundException,
		ScriptException {
		if (engine != null) {
			StringWriter scriptOutput = new StringWriter();
			engine.getContext().setWriter(new PrintWriter(scriptOutput));

			Reader reader = null;
			FileInputStream fin = null;
			try {
				fin = new FileInputStream(file);
				reader = new InputStreamReader(fin, StringUtil.isEmpty(ApplicationInfo.getFileEncoding())
					? Constants.DEFAULT_FILE_ENCODING : ApplicationInfo.getFileEncoding());
				engine.eval(reader);
			} catch (FileNotFoundException e) {
				log.debug("fail to read script file: " + file.getAbsolutePath());
				throw e;
			} catch (ScriptException e) {
				log.debug("fail to execute script file: " + file.getAbsolutePath());
				throw e;
			} finally {
				if (reader != null) {
					try {
						reader.close();
					} catch (IOException e) {
						log.debug("fail to close InputStreamReader : " + file.getAbsolutePath());
					}
				}
				if (fin != null) {
					try {
						fin.close();
					} catch (IOException e) {
						log.debug("fail to close FileInputStream : " + file.getAbsolutePath());
					}
				}
			}
			engine.put(Constants.DEFAULT_JS_DEBUG_MSG_KEYNAME, scriptOutput.getBuffer());
			engineMap.put(engineType, engine);
			ScriptEngineGlobals.addScriptEngineForWebModule(modulePath, engineMap);
		}
	}

	/**
	 * Invoke script.
	 * 
	 * @param modulePath the module path
	 * @param scriptInfos the script infos
	 * @param inputValues the input values
	 * 
	 * @return the object
	 * 
	 * @throws ScriptException the script exception
	 * @throws NoSuchMethodException the no such method exception
	 */
	public static Object invokeScript(String modulePath, List<ScriptInfo> scriptInfos, Object inputValues) throws ScriptException,
		NoSuchMethodException {
		Object newValue = inputValues;

		for (ScriptInfo scriptInfo : scriptInfos) {
			newValue = execute(modulePath, scriptInfo, newValue);
		}

		return newValue;
	}

	/**
	 * Execute.
	 * 
	 * @param modulePath the module path
	 * @param scriptInfo the script info
	 * @param inputValues the input values
	 * 
	 * @return the object
	 * 
	 * @throws ScriptException the script exception
	 * @throws NoSuchMethodException the no such method exception
	 */
	@SuppressWarnings("unchecked")
	public static Object execute(String modulePath, ScriptInfo scriptInfo, Object inputValues) throws ScriptException,
		NoSuchMethodException {
		Map<String, ScriptEngine> engineMap = ScriptEngineGlobals.getScriptEngineForWebModule(modulePath);

		if (engineMap == null) {
			throw new ScriptException("don't exist script engines");
		}

		ScriptEngine scriptEngine = engineMap.get(scriptInfo.getScriptType());

		if (scriptEngine == null) {
			throw new ScriptException("don't exist script engine");
		}

		return invokeScript(scriptEngine, scriptInfo.getFunctionName(), inputValues);
	}

	// private static final Object FAIL = new Object(); // clarify execution
	// result

	/**
	 * Invoke script.
	 * 
	 * @param scriptEngine the script engine
	 * @param functionName the function name
	 * @param inputValue the input value
	 * 
	 * @return the object
	 * 
	 * @throws ScriptException the script exception
	 * @throws NoSuchMethodException the no such method exception
	 */
	public static Object invokeScript(ScriptEngine scriptEngine, String functionName, Object inputValue) throws ScriptException,
		NoSuchMethodException {
		try {
			javax.script.Invocable invocableEngine = (javax.script.Invocable)scriptEngine;
			return invocableEngine.invokeFunction(functionName, inputValue);
		} catch (ScriptException e) {
			log.debug("fail to execute script " + functionName, e);
			throw e;
		} catch (NoSuchMethodException e) {
			log.debug("fail to find script " + functionName, e);
			throw e;
		} finally {
			printJSDebugMessage(scriptEngine);
		}
	}

	/**
	 * Process control script.
	 * 
	 * @param modulePath the module path
	 * @param scriptInfo the script info
	 * @param requestDataMap the request data map
	 * 
	 * @throws ScriptException the script exception
	 * @throws NoSuchMethodException the no such method exception
	 */
	@SuppressWarnings("unchecked")
	public static void processControlScript(String modulePath, ScriptInfo scriptInfo, Map<String, Object> requestDataMap) throws ScriptException,
		NoSuchMethodException {
		Map<String, ScriptEngine> engineMap = ScriptEngineGlobals.getScriptEngineForWebModule(modulePath);

		if (engineMap != null) {
			ScriptEngine scriptEngine = engineMap.get(scriptInfo.getScriptType());

			if (scriptEngine != null) {
				String params = scriptInfo.getParams();
				String returnId = scriptInfo.getReturnId();
				Object outputValue = null;

				if (params == null) {
					outputValue = invokeScript(scriptEngine, scriptInfo.getFunctionName(), requestDataMap);
				} else {
					outputValue = invokeScript(scriptEngine, scriptInfo.getFunctionName(), params);
				}

				if (outputValue != null && returnId != null) {
					requestDataMap.put(returnId, outputValue);
				}
			}
		}
	}

	/**
	 * Prints the js debug message.
	 * 
	 * @param scriptEngine the script engine
	 */
	public static void printJSDebugMessage(ScriptEngine scriptEngine) {
		log.debug("======== script debug message =========");
		StringBuffer buf = (StringBuffer)scriptEngine.get(Constants.DEFAULT_JS_DEBUG_MSG_KEYNAME);
		log.debug(buf.toString());
		log.debug("=======================================");
		buf.delete(0, buf.length());
		scriptEngine.put(Constants.DEFAULT_JS_DEBUG_MSG_KEYNAME, buf);
	}
}
